package com.keguoyu.easymvp.compiler;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.MirroredTypeException;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;

import com.google.auto.service.AutoService;
import com.google.gson.Gson;
import com.keguoyu.easymvp.annotation.ForInvoker;
import com.keguoyu.easymvp.annotation.InvokeBy;
import com.keguoyu.easymvp.compiler.base.BaseProcessor;
import com.keguoyu.easymvp.compiler.base.Invocation;
import com.keguoyu.easymvp.compiler.base.InvokeMethod;

@AutoService(Processor.class)
public class InvokerProcessor extends BaseProcessor {
  public static final String DEFAULT_FILE_NAME = "META-INF/services/inject/invoker_info";
  // 多轮apt只能创建一次文件，只能保存writer
  private Writer mWriter;

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
  }

  @Override
  public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
    Set<Invocation> invocations = new HashSet<>();
    for (Element method : roundEnv.getElementsAnnotatedWith(InvokeBy.class)) {
      if (method == null || method.getKind() != ElementKind.METHOD) {
        continue;
      }
      Set<Modifier> modifiers = method.getModifiers();
      if (!modifiers.contains(Modifier.PUBLIC) || !modifiers.contains(Modifier.STATIC)) {
        mMessager.printMessage(Diagnostic.Kind.ERROR,
            "方法必须是 public static的。" + method.getSimpleName(), method);
        continue;
      }
      InvokeBy invokeBy = method.getAnnotation(InvokeBy.class);
      if (invokeBy == null) {
        continue;
      }
      Element clazz = method.getEnclosingElement();
      if (clazz == null || clazz.getKind() != ElementKind.CLASS) {
        continue;
      }
      // 构建Invocation关系
      Invocation invocation = new Invocation();
      invocation.mTarget = new InvokeMethod();
      invocation.mTarget.className = clazz.toString();
      invocation.mTarget.methodName = method.getSimpleName().toString();
      invocation.mInvoker = new InvokeMethod();
      // https://area-51.blog/2009/02/13/getting-class-values-from-annotations-in-an-annotationprocessor/
      // 获得invoker类名
      try {
        invokeBy.invokerClass();
      } catch (MirroredTypeException e) {
        invocation.mInvoker.className = e.getTypeMirror().toString();
      }
      // 通过methodId 反査到对应的
      String methodId = invokeBy.methodId();
      TypeElement invokerElement = mElementUtils.getTypeElement(invocation.mInvoker.className);
      for (Element invokerMethod : invokerElement.getEnclosedElements()) {
        if (invokerMethod == null || invokerMethod.getKind() != ElementKind.METHOD) {
          continue;
        }
        if (Optional.ofNullable(invokerMethod.getAnnotation(ForInvoker.class))
            .map(forInvoker -> forInvoker.methodId().equals(methodId)).orElse(false)) {
          invocation.mInvoker.methodName = invokerMethod.getSimpleName().toString();
          invocation.mIsStatic = invokerMethod.getModifiers().contains(Modifier.STATIC);
          List<? extends VariableElement> params =
              ((ExecutableElement) invokerMethod).getParameters();
          if (params != null) {
            for (VariableElement param : params) {
              invocation.mParamTypes.add(param.asType().toString());
            }
          }
          if (!checkParameters((ExecutableElement) method, (ExecutableElement) invokerMethod,
              invokerElement, invocation.mIsStatic)) {
            mMessager.printMessage(Diagnostic.Kind.ERROR, "方法参数不匹配, ", method);
          }
        }
      }
      invocations.add(invocation);
    }

    if (invocations.isEmpty()) {
      return false;
    }
    boolean success = true;
    Writer writer = openWriter();
    try {
      Gson gson = new Gson();
      List<Invocation> invocationList = new ArrayList<>(invocations);
      invocationList.sort((in1, in2) -> in1.mTarget.className.compareTo(in2.mTarget.className));
      String json = gson.toJson(invocationList);
      writer.append(json);
      writer.append("\n");
      writer.flush();
      mMessager.printMessage(Diagnostic.Kind.NOTE, json);
    } catch (IOException e) {
      success = false;
    }
    mMessager.printMessage(Diagnostic.Kind.NOTE, "done " + success);
    return success;
  }

  private boolean checkParameters(ExecutableElement method, ExecutableElement invokerMethod,
      TypeElement invokerElement, boolean isStatic) {
    List<? extends VariableElement> targetParameter = method.getParameters();
    List<? extends VariableElement> invokerParameter = invokerMethod.getParameters();
    if (isStatic) {
      return checkParameterAssignability(targetParameter, invokerParameter, 0);
    } else {
      if (!mTypeUtils.isAssignable(invokerElement.asType(), targetParameter.get(0).asType())) {
        return false;
      }
      return checkParameterAssignability(targetParameter, invokerParameter, 1);
    }
  }

  private boolean checkParameterAssignability(List<? extends VariableElement> targetParameter,
      List<? extends VariableElement> invokerParameter, int offset) {
    if (targetParameter.size() - offset != invokerParameter.size()) {
      return false;
    }
    for (int i = 1; i < targetParameter.size(); ++i) {
      // invoker 的参数能够直接传给对应的 target
      if (!mTypeUtils.isAssignable(invokerParameter.get(i - offset).asType(),
          targetParameter.get(i).asType())) {
        return false;
      }
    }
    return true;
  }


  @Override
  protected void finalize() throws Throwable {
    super.finalize();
    if (mWriter != null) {
      mWriter.close();
    }
  }

  private Writer openWriter() {
    if (mWriter != null) {
      return mWriter;
    }
    FileObject source = null;
    try {
      source = mFiler.createResource(StandardLocation.CLASS_OUTPUT, "",
          DEFAULT_FILE_NAME);
      mMessager.printMessage(Diagnostic.Kind.NOTE, source.getName());
      mWriter = source.openWriter();
    } catch (Exception e) {
      e.printStackTrace();
    }
    return mWriter;
  }

  @Override
  public Set<String> getSupportedAnnotationTypes() {
    Set<String> set = new HashSet<>();
    set.add(InvokeBy.class.getName());
    return set;
  }

  @Override
  public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.RELEASE_8;
  }

}
